《Linux – Linux高级编程 – 第三部分 网络编程》第2章 TCP/IP讲解

2.1 TCP/IP 数据包格式解析

TCP/IP 数据包格式解析如下所示:

gdONGj.png

图1

图中括号中的数字代表的是当前域所占的空间大小,单位是bit位。
橙色的是数据链路层的头部,一共14字节
蓝色的部分是IP头部,一般是20字节
紫色部分是TCP头部,一般是20字节
最内部的是数据包内容

橙色部分:链路层
目的MAC:当前step目的主机的mac地址
源MAC:当前step的源主机的mac地址
类型:指定网络层所用的协议类型,通常是IP协议,0x0800

蓝色部分:网络层,这里用的是IP包头格式
版本:记录数据报属于哪一个版本的协议,如IPv4或IPv6
首部长度:指明IP头部长度,单位是字,也就是两个字节。该域的值最小为5,就是标准的头部长度;最大为15,表明有扩展部分。
服务类型:用来区分不同服务的需要
数据报总长:包含IP头部的数据报的总长度。注意,这里不包括链路层的头部,目前最大值是65535字节。
分组ID:这个域的作用是当一个大的数据报被拆分时,拆分成的小的数据段的这个域都是一样的。
标记:共三个bit,第一个未使用;第二个DF(Don’t Fragment),设置成1表示这个数据包不能被分割,这个是针对路由器的一条指令;第三个MF(MoreFragment),如果一个数据包被分割了,那么除了最后一个分段以外的所有分段都必须设置为1,用来表示后面还有更多的分段没有到达,最后一个设置为0,用来表示分割的段全部到达。
段偏移量:这个域有13bit,也就是每一个数据报最多有8192个分段。每一个分段的长度必须是8字节的倍数,也就是说8字节是分段的基本单位,当然分组的最后一个段不做限制。这样最大的数据报长度为8 * 8192=65536字节,比目前限制的最大数据报长度还多1,能够满足对网络中所有数据报传送的需求。
生存时间:这是一个生存期计数器,最大为255s,但是实际上使用的时候用作跳数计数器,当值为0时数据报被丢弃,用来避免一个数据报过久的逗留在网络中。
高层协议:这里和链路层的类型作用相同,用来表示更高层的协议,这个数据报里是TCP
首部校验和:IP头部的校验和
源IP地址:数据报来源主机的IP地址
目的IP地址:数据报目的主机的IP地址

紫色部分:传输层,这里用的是TCP协议
源端口号:数据报来源主机的端口号
目的端口号:数据报目的主机的端口号
注意:源IP地址,目的IP地址,源端口号,目的端口号这四个字段唯一的确定了一个TCP链接。
TCP序号(sq):发送的TCP的序号,从0开始,实际中这个值就是发送的数据报中内容的字节数,比如我发送的第一个报中sq=0,数据报内容20字节,那么下一个数据报的sq就应该是21。
捎带的确认(ack):确认收到上一个数据报,然后act的值是指定自己想要收到的下一个数据报的sq,比如我收到一个数据报的sq=0,数据报内容20字节,那么我的ack就应该是21,用来标明我sq=0,内容为20字节的数据报已经收到,我接下来期望收到的是sq=21的数据报。
首部长度:和IP头部的长度域类似,这个域用来标明TCP头部的长度,单位也是字。
保留:6bit未使用的域
Flag:从左到右,[URG|ACK|PSH|RST|SYN|FIN]
ACK 设置为1表示前面的确认(ack)是有效的,否则前面的确认应被忽略。
PSH 表示要求对方在接到数据后立即请求递交给应用程序,而不是缓冲起来直到缓冲区收满为止。
RST 用于重置一个已经混乱的连接。
SYN 用于建立连接的过程。在链接请求中,SYN=1和ACK=0表示该数据段没有使用捎带的确认域。链接应答则捎带了一个确认,即SYN=1和ACK=1.本质上SYN位是用来表示CONNECTION REQUEST和CONNECTION ACCEPTED,然后进一步用ACK来区分是请求还是应答,的确很高明。
FIN 用来释放一个连接。它表示发送方已经没有数据要传输了。然后,在关闭一个连接后,关闭进程可能会在一段不确定的时间内继续接收到数据。SYN和FIN数据段都有TCP序号,从而保证了这两种数据段被按照正确的顺序来进行处理。
窗口大小:指定了从被确认的字节算起可以发送多少个字节。要深入理解这个域的含义,可以参看TCP用色控制和慢启动算法
校验和:校验范围包括TCP头、数据报内容和概念性伪头部。概念性伪头部又包括源IP,目的IP,TCP协议号。
紧急指针:指向数据报中紧急数据最后一个字节的下一个字节。

2.2利用TCP/IP 模型分析数据传输过程

TCP/IP参考模型是一个非常基础,而且也非常重要的基础框架,要想入门数通这是个必须掌握的基本概念,本文档通过一个简单的示例,结合参考模型来分析一下数据通信的基本过程。

gdXx39.md.png

图2

网络环境非常简单,如下图所示,我们现在来分析一下PC去访问Webserver的WEB服务,整个数据通信过程是如何发生的,为了简化描述,我们这里暂时忽略DNS、ARP、帧校验等等机制的工作细节,只考虑较为宏观的层面。

gdjFAO.png

图3

1)PC访问WebServer的WEB服务,实际上是访问Webserver的HTTP服务。这个过程对于人来说,就是在PC浏览器里输入了Webserver的IP地址或域名,这个行为在PC的应用层面将触发本地的HTTP进程产生一些数据,我们把这些数据称为DATA,它是HTTP的有效荷载:

gdjA4e.md.png

图4

2)数通的最终任务是,要帮助PC把这个HTTP的有效荷载传递到Webserver上的HTTP进程中。这是一个看起来简单的任务,但是实际上,这份数据却要翻山越岭。PC的应用层将这份HTTP的有效荷载交给“传输层”(我们这里忽略TCP三次握手等内容),传输层会为应用层下来的这份数据封装上一个报文头部,由HTTP是基于TCP的应用,因此这里压上的是TCP的头部,在这个头部之中,有目的端口号80,这个端口号将在数据到达Webserver后告诉对端,我要访问你啥服务。当然,为了让这份数据能够可靠的被传输,TCP头部里还有其他重要的内容,这里暂不赘述。

gdjegA.png

图5

3)好了,HTTP的荷载被封装上了TCP的头,为了让这份数据能够在IP网络中进行传输,我们还需要一个“信封”,于是数据到了PC的“因特网层”,在这一层,数据被封装上了一个IP报文头部,在IP包头中,写入了源和目的IP地址,源IP地址为PC的IP:192.168.1.1,而目的IP地址是WebServer的IP:192.168.2.1。IP包头中的另一个重要的字段是协议号,这里写入的值为6,这个值对应着IP头后面封装的协议,也就是TCP。好了,有了IP头这个信封,我们这份数据,就能够在IP网络中被从源传递到目的地:

gdjuut.png

图6

4)然而光有信封还是不够的,至少,我们要把这个信件一段链路一段链路的搬运过去,而不能一下就从源直接穿越到目的地去吧,也不是天朝的穿越剧不是?那咋办,我们还需要一个数据链路层的头部,由于这里是以太网的环境、以太网的链路,因此上层下来的数据,又被封装上了一个以太网帧头,这是为了使得PC能够将这份数据传递到同在链路上的网关R1(的F0/0口)。由于PC设置的网关地址为192.168.1.254,也就是R1的F0/0口IP地址,因此,当访问Webserver 192.168.2.1这个非本地网络的IP时,PC要求助于它的网关,因此再数据链路层面上,PC要数据传递到网关,它将封装上去的以太网头部中写入源MAC也就是自己的MAC:00DD.F800.0001,同时写入目的MAC也就是路由器R1的F0/0口的MAC:000.AAAA.0001,当然如果此刻PC没有网关IP对应的MAC,那么它会发送ARP消息去请求。

以太网帧头中还有一个重要的字段,是类型字段,类型字段用于描述我这个以太网的帧头后面被封装的是什么报文,这里写入的值是0x0800,表示后面是一个IP报文:红

gdjMHf.png

图7

5)费了好大的劲儿,层层加料,终于,这份数据最终做好了传输的准备,从PC传输到了同在链路上的R1,距离目的地又更近了一点,当然,在数据在传输过程中,是不可能像我们图画的这么文艺的,它应该是一些电气化的信息,例如1010101神马的,不鸟他了,反正是这一坨东西是传到了R1:

gdj1US.png

图8

6)R1的F0/0口收到了这份东西,先把它还原成数据帧,查看帧头,发现目的MAC地址正是自己F0/0口的MAC地址,高兴坏了,以为是谁写给自己的情书呢,于是结合查看类型字段,发现是0800,于是知道上层被封装的是一个IP包,它将以太网帧头剥去,将里头的IP报文交上去给IP协议栈处理:

gdjYgs.png

图9

7)接下去是R1的因特网层的工作了,他收到下层传递够来的IP包,查看IP包的目的IP地址,发现目的地是192.168.2.1,我艹,原来不是给我的是给别人的,没办法,R1拿着这个地址去自己的地图–路由表中去查找,发现有个目的地192.168.2.0/24的网络,出口是自己的FA1/0口,下一跳地址是192.168.12.2也就是R2:

gdjUuq.png

图10

8)发现数据包目的IP地址不是自己的R1,找到将数据送到目的地的路径,是交给离目的地更近的192.168.12.2,而为了将数据交给同在链路上的192.168.12.2,又得将数据重新封装上以太网的帧头,这次帧头中的源MAC填写的是R1的FA1/0口的MAC地址,而目的MAC写的是R2的F0/0口的MAC地址:

gdjdbV.png

图11

9)妥妥儿的,数据又被R1传递给了R2:

gdjD5F.png

图12

10)R2收到这个数据后,同样的是先还原成数据帧,然后查看帧头,结果发现目的MAC是自己的MAC,也挺高兴,将数据帧丢给上层的IP协议去处理:

gdjy8J.png

图13

11)结果一样的,丫一看IP头中的目的IP地址,擦类又不是给自己的,咦,为什么要说又字?不管了,反正不是给自己的就对了:

gdjcvR.png

图14

12)于是查路由表,发现目的IP地址192.168.2.1就是在自己FA1/0口直连的网络192.168.2.0/24中的一个IP地址,好办了,缘来是家门口的人啊。于是它将数据再封装上以太网帧头,源MAC是自己Fa1/0口的MAC地址,目的MAC是Webserver的MAC,如果没有Webserver的192.168.2.1对应的MAC,同样的,还是发送ARP消息去请求:

gdj4UO.png

图15

13)数据有上路了,传递给了Webserver

gdjoPe.png

图16

14)说好的宏观分析的,说着说着有变成微观的了。Webserver收到这个数据帧后,查看帧头,目的MAC是自己的网卡MAC,而且类型字段为0800:

gdjqKI.png

图17

15)于是将这个帧头拆开,将里头的IP报文交给IP协议去处理。接着IP协议分析这个IP包,查看包头中的目的IP地址,发现正是自己的网卡IP相同,又发现IP头中的协议号是6,说明这IP头里包裹着的是一个TCP的报文:

gdvpGQ.png

图18

16)知道IP头后面包裹的是一个TCP报文后,它将IP头剥去,将里头的TCP包拿出来,发现TCP头部中目的端口号是80,这是一个well-known众所周知的端口号:

gdv92j.png

图19

17)80端口号对应的服务是HTTP。PC发现,自己的80端口正好是打开的,HTTP服务正在工作,于是将TCP头部摘掉,露出了里头的有效荷载,哎,终于……小姑娘终于又出来了,最终被交给了HTTP服务。这样,一份数据最终就被传递到了目的地的目的应用中。当然,这一过程中我们依然省略了大量的细节。值得注意的是数据通信的过程是双向的,因此PC发送数据到了WebServer,为了让服务交互能够正常进行,数据还会回程,因此实际上还有一个数据返程的过程这里我们就不再分析了,原理大同小异。

gdvFrq.png

图20

2.3网络编程(TCP 协议三次握手过程)

TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的通信协议,数据在传输前要建立连接,传输完毕后还要断开连接。

客户端在收发数据前要使用 connect() 函数和服务器建立连接。建立连接的目的是保证IP地址、端口、物理链路等正确无误,为数据的传输开辟通道。

TCP建立连接时要传输三个数据包,俗称三次握手(Three-way Handshaking)。可以形象的比喻为下面的对话:

  • [Shake 1] 套接字A:“你好,套接字B,我这里有数据要传送给你,建立连接吧。”
  • [Shake 2] 套接字B:“好的,我这边已准备就绪。”
  • [Shake 3] 套接字A:“谢谢你受理我的请求。”

我们先来看一下TCP数据报的结构:

gdvVaT.png

图21

带阴影的几个字段需要重点说明一下:
1) 序号:Seq(Sequence Number)序号占32位,用来标识从计算机A发送到计算机B的数据包的序号,计算机发送数据时对此进行标记。
2) 确认号:Ack(Acknowledge Number)确认号占32位,客户端和服务器端都可以发送,Ack = Seq + 1。
3) 标志位:每个标志位占用1Bit,共有6个,分别为 URG、ACK、PSH、RST、SYN、FIN,具体含义如下:
URG( urgent紧急):紧急指针(urgent pointer)有效。
ACK( acknowledgement 确认) :确认序号有效。
PSH( push传送):接收方应该尽快将这个报文交给应用层。
RST:重置连接。
SYN:建立一个新连接。
FIN( finish结束) :断开一个连接。

对英文字母缩写的总结:Seq 是 Sequence 的缩写,表示序列;Ack(ACK) 是 Acknowledge 的缩写,表示确认;SYN 是 Synchronous 的缩写,愿意是“同步的”,这里表示建立同步连接;FIN 是 Finish 的缩写,表示完成。

2.3.1 TCP连接的建立(三次握手)

使用 connect() 建立连接时,客户端和服务器端会相互发送三个数据包,请看下图:

gdvuRJ.jpg

图22

客户端调用 socket() 函数创建套接字后,因为没有建立连接,所以套接字处于CLOSED状态;服务器端调用 listen() 函数后,套接字进入LISTEN状态,开始监听客户端请求。这个时候,客户端开始发起请求。

【注】为了方便描述我们把客户端命名为A,服务器为B。
第一次握手
当客户端调用 connect() 函数后,TCP协议会组建一个数据包,并设置 SYN 标志位,表示该数据包是用来建立同步连接的。同时生成一个随机数字 1000( 完全是个随机数,一个主机有可能同时要进行与多个主机之间的联机),填充“序号(Seq)”字段,表示该数据包的序号。完成这些工作,开始向服务器端发送数据包,客户端就进入了SYN-SEND状态。

第二次握手
服务器端收到数据包,检测到已经设置了 SYN 标志位,就知道这是客户端发来的建立连接的“请求包”。服务器端也会组建一个数据包,并设置 SYN 和 ACK 标志位,SYN 表示该数据包用来建立连接,ACK 用来确认收到了刚才客户端发送的数据包。

服务器生成一个随机数 2000,填充“序号(Seq)”字段。2000 和客户端数据包没有关系。
服务器将客户端数据包序号(1000)加1,得到1001,并用这个数字填充“确认号(Ack)”字段。服务器将数据包发出,进入SYN-RECV状态。

第三次握手
客户端收到数据包,检测到已经设置了 SYN 和 ACK 标志位,就知道这是服务器发来的“确认包”。客户端会检测“确认号(Ack)”字段,看它的值是否为 1000+1,如果是就说明连接建立成功。
接下来,客户端会继续组建数据包,并设置 ACK 标志位,表示客户端正确接收了服务器发来的“确认包”。同时,将刚才服务器发来的数据包序号(2000)加1,得到 2001,并用这个数字来填充“确认号(Ack)”字段。

客户端将数据包发出,进入ESTABLISED状态,表示连接已经成功建立。

服务器端收到数据包,检测到已经设置了 ACK 标志位,就知道这是客户端发来的“确认包”。服务器会检测“确认号(Ack)”字段,看它的值是否为 2000+1,如果是就说明连接建立成功,服务器进入ESTABLISED状态。

至此,客户端和服务器都进入了ESTABLISED状态,连接建立成功,接下来就可以收发数据了。

三次握手的关键是要确认对方收到了自己的数据包,这个目标就是通过“确认号(Ack)”字段实现的。计算机会记录下自己发送的数据包序号 Seq,待收到对方的数据包后,检测“确认号(Ack)”字段,看Ack = Seq + 1是否成立,如果成立说明对方正确收到了自己的数据包。

2.3.2 TCP数据的传输过程

建立连接后,两台主机就可以相互传输数据了。如下图所示:

gdv8Z6.jpg

图23 TCP 套接字的数据交换过程

上图给出了主机A分2次(分2个数据包)向主机B传递200字节的过程。首先,主机A通过1个数据包发送100个字节的数据,数据包的 Seq 号设置为 1200。主机B为了确认这一点,向主机A发送 ACK 包,并将 Ack 号设置为 1301。

为了保证数据准确到达,目标机器在收到数据包(包括SYN包、FIN包、普通数据包等)包后必须立即回传ACK包,这样发送方才能确认数据传输成功。

此时 Ack 号为 1301 而不是 1201,原因在于 Ack 号的增量为传输的数据字节数。假设每次 Ack 号不加传输的字节数,这样虽然可以确认数据包的传输,但无法明确100字节全部正确传递还是丢失了一部分,比如只传递了80字节。因此按如下的公式确认 Ack 号:
Ack号 = Seq号 + 传递的字节数 + 1

与三次握手协议相同,最后加 1 是为了告诉对方要传递的 Seq 号。

下面分析传输过程中数据包丢失的情况,如下图所示:

gdvJIO.jpg

图24 TCP套接字数据传输过程中发生错误

上图表示通过 Seq 1301 数据包向主机B传递100字节的数据,但中间发生了错误,主机B未收到。经过一段时间后,主机A仍未收到对于 Seq 1301 的ACK确认,因此尝试重传数据。

为了完成数据包的重传,TCP套接字每次发送数据包时都会启动定时器,如果在一定时间内没有收到目标机器传回的 ACK 包,那么定时器超时,数据包会重传。

上图演示的是数据包丢失的情况,也会有 ACK 包丢失的情况,一样会重传。

重传超时时间(RTO, Retransmission Time Out)

这个值太大了会导致不必要的等待,太小会导致不必要的重传,理论上最好是网络 RTT 时间,但又受制于网络距离与瞬态时延变化,所以实际上使用自适应的动态算法(例如 Jacobson 算法和 Karn 算法等)来确定超时时间。

往返时间(RTT,Round-Trip Time)表示从发送端发送数据开始,到发送端收到来自接收端的 ACK 确认包(接收端收到数据后便立即确认),总共经历的时延。

重传次数

TCP数据包重传次数根据系统设置的不同而有所区别。有些系统,一个数据包只会被重传3次,如果重传3次后还未收到该数据包的 ACK 确认,就不再尝试重传。但有些要求很高的业务系统,会不断地重传丢失的数据包,以尽最大可能保证业务数据的正常交互。

2.3.3 TCP四次握手断开连接

建立连接非常重要,它是数据正确传输的前提;断开连接同样重要,它让计算机释放不再使用的资源。如果连接不能正常断开,不仅会造成数据传输错误,还会导致套接字不能关闭,持续占用资源,如果并发量高,服务器压力堪忧。

建立连接需要三次握手,断开连接需要四次握手,可以形象的比喻为下面的对话:
[Shake 1] 套接字A:“任务处理完毕,我希望断开连接。”
[Shake 2] 套接字B:“哦,是吗?请稍等,我准备一下。”
等待片刻后……
[Shake 3] 套接字B:“我准备好了,可以断开连接了。”
[Shake 4] 套接字A:“好的,谢谢合作。”

下图演示了客户端主动断开连接的场景:

gdvtiD.jpg

图25

建立连接后,客户端和服务器都处于ESTABLISED状态。这时,客户端发起断开连接的请求:
1) 客户端调用 close() 函数后,向服务器发送 FIN 数据包,进入FIN_WAIT_1状态。FIN 是 Finish 的缩写,表示完成任务需要断开连接。
2) 服务器收到数据包后,检测到设置了 FIN 标志位,知道要断开连接,于是向客户端发送“确认包”,进入CLOSE_WAIT状态。

注意:服务器收到请求后并不是立即断开连接,而是先向客户端发送“确认包”,告诉它我知道了,我需要准备一下才能断开连接。

3) 客户端收到“确认包”后进入FIN_WAIT_2状态,等待服务器准备完毕后再次发送数据包。
4) 等待片刻后,服务器准备完毕,可以断开连接,于是再主动向客户端发送 FIN 包,告诉它我准备好了,断开连接吧。然后进入LAST_ACK状态。
5) 客户端收到服务器的 FIN 包后,再向服务器发送 ACK 包,告诉它你断开连接吧。然后进入TIME_WAIT状态。
6) 服务器收到客户端的 ACK 包后,就断开连接,关闭套接字,进入CLOSED状态。

关于 TIME_WAIT 状态的说明

客户端最后一次发送 ACK包后进入 TIME_WAIT 状态,而不是直接进入 CLOSED 状态关闭连接,这是为什么呢?

TCP 是面向连接的传输方式,必须保证数据能够正确到达目标机器,不能丢失或出错,而网络是不稳定的,随时可能会毁坏数据,所以机器A每次向机器B发送数据包后,都要求机器B”确认“,回传ACK包,告诉机器A我收到了,这样机器A才能知道数据传送成功了。如果机器B没有回传ACK包,机器A会重新发送,直到机器B回传ACK包。

客户端最后一次向服务器回传ACK包时,有可能会因为网络问题导致服务器收不到,服务器会再次发送 FIN 包,如果这时客户端完全关闭了连接,那么服务器无论如何也收不到ACK包了,所以客户端需要等待片刻、确认对方收到ACK包后才能进入CLOSED状态。那么,要等待多久呢?

数据包在网络中是有生存时间的,超过这个时间还未到达目标主机就会被丢弃,并通知源主机。这称为报文最大生存时间(MSL,Maximum Segment Lifetime)。TIME_WAIT 要等待 2MSL 才会进入 CLOSED 状态。ACK 包到达服务器需要 MSL 时间,服务器重传 FIN 包也需要 MSL 时间,2MSL 是数据包往返的最大时间,如果 2MSL 后还未收到服务器重传的 FIN 包,就说明服务器已经收到了 ACK 包。

2.3.4优雅的断开TCP连接

调用 close()/closesocket() 函数意味着完全断开连接,即不能发送数据也不能接收数据,这种“生硬”的方式有时候会显得不太“优雅”。

gdvURH.jpg

图26 close()/closesocket() 断开连接

上图演示了两台正在进行双向通信的主机。主机A发送完数据后,单方面调用 close()/closesocket() 断开连接,之后主机A、B都不能再接受对方传输的数据。实际上,是完全无法调用与数据收发有关的函数。

一般情况下这不会有问题,但有些特殊时刻,需要只断开一条数据传输通道,而保留另一条。使用 shutdown() 函数可以达到这个目的,它的原型为:

int shutdown(int sock, int howto);  //Linux
int shutdown(SOCKET s, int howto);  //Windows

【参数说明】sock 为需要断开的套接字,howto 为断开方式。
howto 在 Linux 下有以下取值
 SHUT_RD:断开输入流。套接字无法接收数据(即使输入缓冲区收到数据也被抹去),无法调用输入相关函数。
 SHUT_WR:断开输出流。套接字无法发送数据,但如果输出缓冲区中还有未传输的数据,则将传递到目标主机。
 SHUT_RDWR:同时断开 I/O 流。相当于分两次调用 shutdown(),其中一次以 SHUT_RD 为参数,另一次以 SHUT_WR 为参数。

howto 在 Windows 下有以下取值
 SD_RECEIVE:关闭接收操作,也就是断开输入流。
 SD_SEND:关闭发送操作,也就是断开输出流。
 SD_BOTH:同时关闭接收和发送操作。

close()/closesocket()和shutdown()的区别

确切地说,close() / closesocket() 用来关闭套接字,将套接字描述符(或句柄)从内存清除,之后再也不能使用该套接字,与C语言中的 fclose() 类似。应用程序关闭套接字后,与该套接字相关的连接和缓存也失去了意义,TCP协议会自动触发关闭连接的操作。

shutdown() 用来关闭连接,而不是套接字,不管调用多少次 shutdown(),套接字依然存在,直到调用 close() / closesocket() 将套接字从内存清除。

调用 close()/closesocket() 关闭套接字时,或调用 shutdown() 关闭输出流时,都会向对方发送 FIN 包。FIN 包表示数据传输完毕,计算机收到 FIN 包就知道不会再有数据传送过来了。

默认情况下,close()/closesocket() 会立即向网络中发送FIN包,不管输出缓冲区中是否还有数据,而shutdown() 会等输出缓冲区中的数据传输完毕再发送FIN包。也就意味着,调用 close()/closesocket() 将丢失输出缓冲区中的数据,而调用 shutdown() 不会。

Related posts

Leave a Comment